const crypto = require("crypto");
const { DOMParser } = require("xmldom");
const { logger } = require("./serviceLogger");

class WXBizMsgCrypt {
    constructor(token, encodingAesKey, appId) {
        this.token = token;
        this.encodingAesKey = encodingAesKey;
        this.appId = appId;
    }

    encryptMsg(replyMsg, timeStamp, nonce, encryptMsg) {
        const pc = new Prpcrypt(this.encodingAesKey);

        // 加密
        const array = pc.encrypt(replyMsg, this.appId);
        logger.info({}, {
            label: '加密结果：' + array
        });
        const ret = array[0];
        if (ret !== 0) {
            return ret;
        }

        if (!timeStamp || timeStamp == '' || timeStamp == null || timeStamp === undefined) {
            timeStamp = Math.floor(Date.now() / 1000);
        }
        const encrypt = array[1];

        // 生成安全签名
        const sha1 = new SHA1();
        const array2 = sha1.getSHA1(this.token, timeStamp, nonce, encrypt);
        logger.info({}, {
            label: '获取SHA1结果：' + array2
        });
        const ret2 = array2[0];
        if (ret2 !== 0) {
            return ret2;
        }
        const signature = array2[1];

        // 生成发送的xml
        const xmlparse = new XMLParse();
        encryptMsg = xmlparse.generate(encrypt, signature, timeStamp, nonce);
        return { code: ErrorCode.OK, encryptMsg };
    }

    decryptMsg(msgSignature, timestamp = null, nonce, postData, msg) {
        if (this.encodingAesKey.length !== 43) {
            return { code: ErrorCode.IllegalAesKey };
        }

        const pc = new Prpcrypt(this.encodingAesKey);

        // 提取密文
        const xmlparse = new XMLParse();
        const array = xmlparse.extract(postData);
        const ret = array[0];

        if (ret !== 0) {
            return { code: ret };
        }

        if (timestamp == null) {
            timestamp = Math.floor(Date.now() / 1000);
        }

        const encrypt = array[1];
        const touser_name = array[2];

        // 验证安全签名
        const sha1 = new SHA1();
        const array2 = sha1.getSHA1(this.token, timestamp, nonce, encrypt);
        const ret2 = array2[0];
        console.log('ret2:', ret2);

        if (ret2 !== 0) {
            return { code: ret2 };
        }

        console.log('array2:::', array2);
        const signature = array2[1];
        if (signature !== msgSignature) {
            return { code: ErrorCode.ValidateSignatureError };
        }

        const result = pc.decrypt(encrypt, this.appId);
        console.log('result:::', result);
        if (result[0] !== 0) {
            return { code: result[0] };
        }
        msg = result[1];

        return { code: ErrorCode.OK, msg };
    }
}

/**
 * error code 说明.
 * <ul>
 *    <li>-40001: 签名验证错误</li>
 *    <li>-40002: xml解析失败</li>
 *    <li>-40003: sha加密生成签名失败</li>
 *    <li>-40004: encodingAesKey 非法</li>
 *    <li>-40005: appid 校验错误</li>
 *    <li>-40006: aes 加密失败</li>
 *    <li>-40007: aes 解密失败</li>
 *    <li>-40008: 解密后得到的buffer非法</li>
 *    <li>-40009: base64加密失败</li>
 *    <li>-40010: base64解密失败</li>
 *    <li>-40011: 生成xml失败</li>
 * </ul>
 */
class ErrorCode {
    static OK = 0;
    static ValidateSignatureError = -40001;
    static ParseXmlError = -40002;
    static ComputeSignatureError = -40003;
    static IllegalAesKey = -40004;
    static ValidateAppidError = -40005;
    static EncryptAESError = -40006;
    static DecryptAESError = -40007;
    static IllegalBuffer = -40008;
    static EncodeBase64Error = -40009;
    static DecodeBase64Error = -40010;
    static GenReturnXmlError = -40011;
}

/**
 * PKCS7Encoder class
 *
 * Provides encryption and decryption methods based on the PKCS7 algorithm.
 */
class PKCS7Encoder {
    static block_size = 32;

    /**
     * Pads the plaintext with the required number of bytes.
     * @param {string} text - The plaintext to be padded.
     * @returns {string} - The padded plaintext.
     */
    encode(text) {
        const block_size = PKCS7Encoder.block_size;
        const text_length = text.length;
        const amount_to_pad = block_size - (text_length % block_size);
        if (amount_to_pad == 0) {
            amount_to_pad = block_size;
        }
        const pad_chr = String.fromCharCode(amount_to_pad);
        let tmp = "";
        for (let index = 0; index < amount_to_pad; index++) {
            tmp += pad_chr;
        }
        return text + tmp;
    }

    /**
     * Removes the padding from the decrypted plaintext.
     * @param {string} text - The decrypted plaintext.
     * @returns {string} - The plaintext without padding.
     */
    decode(text) {
        let pad = text.charCodeAt(text.length - 1);
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        return text.substring(0, text.length - pad);
    }
}

/**
 * Prpcrypt class
 *
 * Provides encryption and decryption methods for receiving and sending messages to the public platform.
 */
class Prpcrypt {
    constructor(k) {
        this.key = Buffer.from(k + "=", "base64");
        // let truncatedBuffer = Buffer.alloc(16);
        // this.key.copy(truncatedBuffer, 0, 0, 16);
        this.iv = this.key.subarray(0, 16);
    }

    /**
     * Encrypts the plaintext.
     * @param {string} text - The plaintext to be encrypted.
     * @param {string} appid - The app ID.
     * @returns {Array} - An array containing the error code and the encrypted ciphertext.
     */
    encrypt(text, appid) {
        try {
            const random = this.getRandomStr();
            // const randomString = crypto.pseudoRandomBytes(16);
            // const msg = Buffer.from(text);
            // const msgLen = Buffer.alloc(4);
            // msgLen.writeUInt32BE(msg.length, 0);
            // const id = Buffer.from(appid);
            // const bufMsg = Buffer.concat([randomString, msgLen, msg, id]);

            // const textBytes = this.textToBytes(text);
            // const lengthBytes = new Uint8Array([textBytes.length >> 24, textBytes.length >> 16, textBytes.length >> 8, textBytes.length]);
            // const lengthStr = this.bytesToString(lengthBytes);
            // text = random + Buffer.from(text).toString('utf8') + text + appid;

            // const bRand = Buffer.from(random, "utf8");
            // const bAppid = Buffer.from(appid, "utf8");
            // const btmpMsg = Buffer.from(text, "utf8");
            // const bMsgLen = new Uint8Array(4);
            // const tmpMsgLength = btmpMsg.length;
          
            // bMsgLen[0] = (tmpMsgLength >> 24) & 0xff;
            // bMsgLen[1] = (tmpMsgLength >> 16) & 0xff;
            // bMsgLen[2] = (tmpMsgLength >> 8) & 0xff;
            // bMsgLen[3] = tmpMsgLength & 0xff;
          
            // const bMsg = new Uint8Array(bRand.length + bMsgLen.length + bAppid.length + btmpMsg.length);
          
            // bMsg.set(bRand, 0);
            // bMsg.set(bMsgLen, bRand.length);
            // bMsg.set(btmpMsg, bRand.length + bMsgLen.length);
            // bMsg.set(bAppid, bRand.length + bMsgLen.length + btmpMsg.length);

            const pkcEncoder = new PKCS7Encoder();
            const paddedText = pkcEncoder.encode(random + String.fromCharCode(text.length) + text + appid);
            const cipher = crypto.createCipheriv("aes-256-cbc", this.key, this.iv);
            // cipher.setAutoPadding(false);


            // const cipheredMsg = Buffer.concat([cipher.update(encodedBufMsg), cipher.final()])
            // return [0, cipheredMsg.toString('base64')];

            let encrypted = cipher.update(paddedText, "utf8", "base64");

            encrypted += cipher.final("base64");
            return [0, encrypted];
        } catch (error) {
            console.log('Prpcrypt encrypt：', error);
            return [1, null];
        }
    }

    /**
     * Decrypts the ciphertext.
     * @param {string} encrypted - The ciphertext to be decrypted.
     * @param {string} appid - The app ID.
     * @returns {Array} - An array containing the error code and the decrypted plaintext.
     */
    decrypt(encrypted, appid) {
        try {
            const decipher = crypto.createDecipheriv("aes-256-cbc", this.key, this.iv);
            console.log('decipher:', decipher);
            let decrypted = decipher.update(encrypted, "base64", "utf8");
            decrypted += decipher.final("utf8");
            const pkc_encoder = new PKCS7Encoder();
            console.log('pkc_encoder:', pkc_encoder);
            const result = pkc_encoder.decode(decrypted);
            console.log('result：', result);
            const content = result.substring(16);
            const xml_len = content.length;
            const xml_content = content.substring(4, xml_len - 18);
            const from_appid = content.substring(xml_len - 18);
            console.log('Prpcrypt decrypt result：', result);
            console.log('Prpcrypt decrypt content', content);
            console.log('Prpcrypt decrypt xml_len', xml_len);
            console.log('Prpcrypt decrypt xml_content', xml_content);
            console.log('Prpcrypt decrypt from_appid', from_appid);
            if (from_appid !== appid) {
                return [2, null];
            }
            return [0, xml_content.toString('utf-8')];
        } catch (error) {
            console.log('Prpcrypt decrypt', error);
            return [3, null];
        }
    }

    // 将明文转换为字节数组
    textToBytes(text) {
        var bytes = [];
        for (var i = 0; i < text.length; i++) {
            bytes.push(text.charCodeAt(i) & 0xff);
        }
        return bytes;
    }

    // 将字节数组转换为字符串
    bytesToString(bytes) {
        var str = '';
        for (var i = 0; i < bytes.length; i++) {
            str += String.fromCharCode(bytes[i]);
        }
        return str;
    }

    /**
     * Generates a random 16-character string.
     * @returns {string} - The generated string.
     */
    getRandomStr() {
        const str_pol =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
        let str = "";
        const max = str_pol.length - 1;
        for (let i = 0; i < 16; i++) {
            str += str_pol.charAt(Math.floor(Math.random() * max));
        }
        return str;
    }
}

/**
 * SHA1 class
 *
 * 计算公众平台的消息签名接口.
 */
class SHA1 {
    /**
     * 用SHA1算法生成安全签名
     * @param {string} token 票据
     * @param {string} timestamp 时间戳
     * @param {string} nonce 随机字符串
     * @param {string} encrypt_msg 密文消息
     */
    getSHA1(token, timestamp, nonce, encrypt_msg) {
        //排序
        try {
            let array = [encrypt_msg, token, timestamp, nonce];
            array.sort();
            let str = array.join("");
            return [ErrorCode.OK, sha1Encrypt(str)];
        } catch (e) {
            //print e + "\n";
            console.log("sha加密生成签名失败", e);
            return [ErrorCode.ComputeSignatureError, null];
        }
    }
}

const sha1Encrypt = (content) => {
    let hash = crypto.createHash('sha1');
    hash.update(content);
    return hash.digest("hex");
};

/**
 * XMLParse class
 *
 * 提供提取消息格式中的密文及生成回复消息格式的接口.
 */
class XMLParse {
    /**
     * 提取出xml数据包中的加密消息
     * @param {string} xmltext 待提取的xml字符串
     * @returns {Array} 提取出的加密消息数组
     */
    extract(xmltext) {
        try {
            let parser = new DOMParser();
            let xmlDoc = parser.parseFromString(xmltext, "text/xml");
            let encryptNode = xmlDoc.getElementsByTagName("Encrypt")[0];
            let toUserNameNode = xmlDoc.getElementsByTagName("ToUserName")[0];
            let encrypt = encryptNode.textContent;
            let toUserName = toUserNameNode.textContent;
            return [0, encrypt, toUserName];
        } catch (error) {
            logger.error(
                {},
                {
                    label: "解析xml失败：" + JSON.stringify(error),
                }
            );
            return [ErrorCode.ParseXmlError, null, null];
        }
    }

    /**
     * 生成xml消息
     * @param {string} encrypt 加密后的消息密文
     * @param {string} signature 安全签名
     * @param {string} timestamp 时间戳
     * @param {string} nonce 随机字符串
     * @returns {string} 生成的xml消息
     */
    generate(encrypt, signature, timestamp, nonce) {
        let format = `<xml>` +
                `<Encrypt><![CDATA[${encrypt}]]></Encrypt>` +
                `<MsgSignature><![CDATA[${signature}]]></MsgSignature>` +
                `<TimeStamp>${timestamp}</TimeStamp>` +
                `<Nonce><![CDATA[${nonce}]]></Nonce>` +
            `</xml>`;
        return format;
    }
}

module.exports = WXBizMsgCrypt;
